package com.investmenttechnologyservices.util;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.prefs.Preferences;

import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.SwingWorker;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;

public class BulkDataUploader extends JPanel {
	private static final long serialVersionUID = 1L;
	
	private JTextField tfUser;
	private JPasswordField tfPass;
	private JTextField tfAppId;
	private JLabel status;
	
	private BulkDataUploader() {
		super(null);
		int starty = 40;
		
		int x = 40;
		int y = starty;
		
		JLabel label1 = new JLabel("Username");
		label1.setBounds(x, y, 100, 20);
		add(label1);
		y+=40;
		JLabel label2 = new JLabel("Password");
		label2.setBounds(x, y, 100, 20);
		add(label2);
		y+=40;
		JLabel label3 = new JLabel("App ID");
		label3.setBounds(x, y, 100, 20);
		add(label3);
		y+=22;

		JLabel label4 = new JLabel("(Leave blank for Dev server)");
		label4.setBounds(x, y, 200, 20);
		add(label4);
		y+=38;
		
		status = new JLabel("");
		status.setForeground(new Color(180,20,20));
		status.setBounds(x, y, 550, 40);
		add(status);
		
		x = 160;
		y = starty;
		
		tfUser = new JTextField("");
		tfUser.setBounds(x, y, 200, 20);
		add(tfUser);
		y+=40;
		tfPass = new JPasswordField("");
		tfPass.setBounds(x, y, 200, 20);
		add(tfPass);
		y+=40;
		tfAppId = new JTextField("");
		tfAppId.setBounds(x, y, 200, 20);
		add(tfAppId);
		
		loadProperties();
		
		x = 400;
		y = starty;
		
		JButton button = new JButton("Upload data");
		button.setBounds(x, y, 150, 30);
		add(button);
		y+=40;

		JButton buttonBanks = new JButton("Upload Banks");
		buttonBanks.setBounds(x, y, 150, 30);
		add(buttonBanks);
		y+=40;

		JButton buttonSave = new JButton("Save settings");
		buttonSave.setBounds(x, y, 150, 30);
		add(buttonSave);
		y+=40;

		button.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				status.setText("");
				
				String username = tfUser.getText();
				if (isBlank(username)) {
					status.setText("username missing");
					return;
				}

				String wd = getSavedDir();
				JFileChooser fc;
				if (wd != null) {
					fc = new JFileChooser(wd);
				} else {
					fc = new JFileChooser();
				}
				int returnVal = fc.showOpenDialog(BulkDataUploader.this);
		        if (returnVal == JFileChooser.APPROVE_OPTION) {
		            File file = fc.getSelectedFile();
		            saveDir(file);
		            try {
						upload(file);
					} catch (IOException e1) {
						e1.printStackTrace();
					}
		        }
			}
		});

		buttonBanks.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				status.setText("");
				
				String username = tfUser.getText();
				if (isBlank(username)) {
					status.setText("username missing");
					return;
				}

				bankload();
			}
		});

		buttonSave.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				saveProperties();
				status.setText("Saved");
			}
		});
	}

	public static void main(String[] args) throws IOException {
		JFrame f = new JFrame("Upload Data to Google App Engine");
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setSize(650,350);
		f.getContentPane().add(new BulkDataUploader());
		f.setVisible(true);
	}

	private void saveProperties() {
		Preferences userPref = Preferences.userRoot();
		userPref.put("user", tfUser.getText());
		userPref.put("app", tfAppId.getText());
	}

	private void loadProperties() {
		Preferences userPref = Preferences.userRoot();
		String user = userPref.get("user","");
		tfUser.setText(user);
		String appid = userPref.get("app","");
		tfAppId.setText(appid);
	}

	private void upload(final File file) throws IOException {
		final String username = tfUser.getText();
		final char[] password = tfPass.getPassword();
		String appid = tfAppId.getText();
		
		final String host;
		final int port;
		if (isBlank(appid)) {
			host = "localhost";
			port = 8888;
		} else {
			host = appid + ".appspot.com";
			port = 443;
		}
		
		status.setText("Working. please wait...");

		SwingWorker<Boolean,Object> sw = new SwingWorker<Boolean,Object>() {
			@Override
			protected Boolean doInBackground() throws Exception {
				return work(file, username, password, host, port);
			}
			@Override
			protected void done() {
				try {
					Boolean ok = get();
					if (ok) {
						status.setText("Done");
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		};
		sw.execute();
	}

	private void bankload() {
		final String username = tfUser.getText();
		final char[] password = tfPass.getPassword();
		String appid = tfAppId.getText();
		
		final String host;
		final int port;
		if (isBlank(appid)) {
			host = "localhost";
			port = 8888;
		} else {
			host = appid + ".appspot.com";
			port = 443;
		}
		
		status.setText("Working. please wait...");

		SwingWorker<Boolean,Object> sw = new SwingWorker<Boolean,Object>() {
			@Override
			protected Boolean doInBackground() throws Exception {
				return work2(username, password, host, port);
			}
			@Override
			protected void done() {
				try {
					Boolean ok = get();
					if (ok) {
						status.setText("Done");
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		};
		sw.execute();
	}

	private static boolean isBlank(String s) {
		return s==null||s.trim().length()==0;
	}

	private void saveDir(File file) {
		try {
			String wd = file.getParentFile().getCanonicalPath();
			Preferences userPref = Preferences.userRoot();
			userPref.put("wd", wd);
		} catch (IOException e) {
			status.setText("Cannot save last directory for file " + file.getName());
		}
	}
	
	private String getSavedDir() {
		Preferences userPref = Preferences.userRoot();
		return userPref.get("wd",null);
	}
	
	private boolean work(File file, final String username, final char[] password,
			final String host, final int port) throws IOException {
		
		RemoteApiInstaller installer = new RemoteApiInstaller();
		try {
			RemoteApiOptions options = new RemoteApiOptions().server(host, port)
					.credentials(username, new String(password));
			installer.install(options);

			DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
//			System.out.println("Key of new entity is " + ds.put(new Entity("Hello Remote API!")));
			
			ObjectMapper m = new ObjectMapper();
			JsonNode node = m.readTree(new FileInputStream(file));
			for (JsonNode jsonNode : node) {
				handleKind(ds, jsonNode);
			}
			
		} catch (Exception ex) {
			
			status.setText("Error: " + ex.getMessage());
			ex.printStackTrace();
			
			return false;
			
		} finally {
			installer.uninstall();
		}

		return true;
	}

	private void handleKind(DatastoreService ds, JsonNode node) {
		String kind = node.get("kind").textValue();
		
		JsonNode parentKindNode = node.get("parentKind");
		String parentKind = null;
		if (parentKindNode != null && !parentKindNode.isNull()) {
			parentKind = parentKindNode.textValue();
		}

		JsonNode cleanNode = node.get("clean");
		boolean clean = false;
		if (cleanNode != null) {
			clean = cleanNode.asBoolean();
		}
		
		if (clean) {
			DatastoreService s = DatastoreServiceFactory.getDatastoreService();
			PreparedQuery pq = s.prepare(new Query(kind));
			for (Entity ent : pq.asIterable()) {
				System.err.print("deleting:");
				Util.printEntity(ent);
				Transaction tx = ds.beginTransaction();
				s.delete(ent.getKey());
				tx.commit();
			}
		}

		// types: { "dataProviderKey" : "key:dataProvider" }
		
		Map<String, Convertor<?>> convertors = null;
		JsonNode typesNode = node.get("types");
		if (typesNode != null) {
			convertors = new HashMap<String, BulkDataUploader.Convertor<?>>();
			Iterator<Entry<String,JsonNode>> iter = typesNode.fields();
			while (iter.hasNext()) {
				Entry<String,JsonNode> entry = iter.next();
				String propertyName = entry.getKey(); //"dataProviderKey"
				String typeDef = entry.getValue().asText(); //"key:dataProvider"
				convertors.put(propertyName, createConvertor(typeDef));
			}
		}
		
		JsonNode entitiesNode = node.get("entities");
		int count = 0;
		for (JsonNode entityNode : entitiesNode) {
			handleEntity(ds, entityNode, parentKind, kind, convertors);
			count++;
		}
		System.err.println("added " + count + " entities of kind " + kind);

		System.err.println("Waiting for server to flush all updates. please wait....");
		
		int verifiedCount = 0;
		while (verifiedCount < count) {
			PreparedQuery pq = ds.prepare(new Query(kind));
			verifiedCount = pq.countEntities(FetchOptions.Builder.withDefaults());
			System.err.println("verifiedCount = " + verifiedCount);
			try {
				Thread.sleep(4000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		System.err.println("Done.");
	}
	
	private Convertor<?> createConvertor(String typeDef) {
		if (typeDef.startsWith("key:")) {
			String kind = typeDef.substring(4);
			return new KeyConvertor(kind);
		}
		throw new IllegalStateException("no support for " + typeDef);
	}

	private void handleEntity(DatastoreService ds, JsonNode entityNode,
			String parentKind, String kind, Map<String, Convertor<?>> convertors) {
		Key key;
		
		Entity ent;
		
		JsonNode idNode = entityNode.get("id");
		if (idNode != null) {
			String id = idNode.textValue();

			if (parentKind != null) {
				JsonNode parentIdNode = entityNode.get("parentid");
				if (parentIdNode != null) {
					String parentid = parentIdNode.textValue();
					Key parentKey = getParentKey(ds, parentKind, parentid);
					key = KeyFactory.createKey(parentKey, kind, id);
				} else {
					key = KeyFactory.createKey(kind, id);
				}
			} else {
				key = KeyFactory.createKey(kind, id);
			}
			ent = new Entity(key);
		} else {
			ent = new Entity(kind);
		}
		

		Iterator<Entry<String,JsonNode>> iter = entityNode.fields();
		while (iter.hasNext()) {
			Entry<String,JsonNode> entry = iter.next();
			String propertyName = entry.getKey();
			if (!propertyName.equals("id") && !propertyName.equals("parentid")) {
				Object value = null;
				JsonNode valueNode = entry.getValue();
				
				if (convertors != null) {
					Convertor<?> convertor = convertors.get(propertyName);
					if (convertor != null) {
						String s = valueNode.asText();
						value = convertor.convert(s);
					}
				}
				
				if (value == null) {
					if (valueNode.isBoolean()) {
						value = valueNode.asBoolean();
					} else if (valueNode.isLong()) {
						value = valueNode.asLong();
					} else if (valueNode.isInt()) {
						value = valueNode.asInt();
					} else if (valueNode.isTextual()) {
						value = valueNode.asText();
					} else if (valueNode.isDouble()) {
						value = valueNode.asDouble();
					} else {
						System.err.println("Warning: unknown value for entity of kind="+ kind +", property="+valueNode+". will be ignored");
					}
				}
				
				if (value != null) {
					ent.setProperty(propertyName, value);
				}
			}
		}
		
		System.err.print("creating:");
		Util.printEntity(ent);
		
		Transaction tx = ds.beginTransaction();
		ds.put(ent);
		tx.commit();
		
//		for (JsonNode entityNode : entityNode) {
//			
//		}
	}

	Map< String, Map <String, Key> > map = new HashMap<String, Map<String,Key>>();
	
	private Key getParentKey(DatastoreService ds, String parentKind, String parentid) {
		Map<String, Key> keysMap = map.get(parentKind);
		if (keysMap == null) {
			keysMap = new HashMap<String, Key>();
			map.put(parentKind, keysMap);
			Query q = new Query(parentKind).setKeysOnly();
			PreparedQuery pq = ds.prepare(q);
			Iterable<Entity> iter = pq.asIterable();
			for (Entity e : iter) {
				Key k = e.getKey();
				keysMap.put(k.getName(), k);
			}
			System.err.println("-----------------------------------------");
			System.err.println("Built parent key for kind = " + parentKind);
			System.err.println("keys="+keysMap.keySet()+ " size="+keysMap.keySet().size());
			System.err.println("-----------------------------------------");
		}
		Key key = keysMap.get(parentid);
		if (key == null) {
			throw new RuntimeException("Cannot find parent key. parentKind="+parentKind+" id="+parentid);
		}
		return key;
		//KeyFactory.createKey(parentKind, parentid);	
	}

	private static interface Convertor<T> {
		T convert(String s);
	}

	private static class KeyConvertor implements Convertor<Key> {
		private String kind;
		private KeyConvertor(String kind) {
			this.kind = kind;
		}
		public Key convert(String s) {
			return KeyFactory.createKey(kind, s);
		}
	}

	private boolean work2(final String username, final char[] password,
			final String host, final int port) {
		
		Key key;
			
    	Institution[] dlInstitutions = OFXgetBanks.getAll();
    	ArrayList<Institution> institutions = new ArrayList<Institution>() ;
    	
    	if (dlInstitutions.length>0) {
    		// get institution details	
    		try {
    			for (Institution i : dlInstitutions) {
    				Institution fullInstitution = OFXgetBanks.downloadInstitution(i.getId());
        			if (fullInstitution != null) {
        				i.setName(fullInstitution.getName());
	        			i.setFId(fullInstitution.getFId());
	        			i.setOrg(fullInstitution.getOrg());
	        			i.setUrl(fullInstitution.getUrl());	
	        			i.setBrokerId(fullInstitution.getBrokerId());
	        			institutions.add(i);
	        			System.err.print("Got: Id= " +i.getId() + ", name= " +i.getName() + ", Org= " +i.getOrg() + "\n");
        			} else {
	    				System.err.print("SKIPPED: name= " +i.getName() +"\n");
        			}
    			}
    		}
    		catch (IOException e) {
    			System.out.println("Exception: " + e);
    			return false;
    		}
        }

    	RemoteApiInstaller installer = new RemoteApiInstaller();
		try {
			
			String kind = "bank";
			
			RemoteApiOptions options = new RemoteApiOptions().server(host, port)
					.credentials(username, new String(password));
			installer.install(options);

			DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
			
			// remove previously autoloaded banks
			Query q = new Query(kind);
			q.setFilter(new FilterPredicate("sourcedFromOFX",FilterOperator.EQUAL,true));
			PreparedQuery pq = ds.prepare(q);
			
			for (Entity ent : pq.asIterable()) {
				System.err.print("deleting:");
				Util.printEntity(ent);
				ds.delete(ent.getKey());
			}

			for (Institution i : institutions) {

				String id;
				if( i.getId()==null||i.getId().trim().length()==0 ) {
					id = RandomStringId.setId(10);
				} else {
					id = i.getId();
				}
				
				key = KeyFactory.createKey(kind, id);

				Entity ent = new Entity(key);
				ent.setProperty("name", i.getName());
				ent.setProperty("fiId", i.getFId());
				ent.setProperty("org", i.getOrg());
				ent.setProperty("fiUrl", i.getUrl());
				ent.setProperty("brokerId", i.getBrokerId());
				ent.setProperty("sourcedFromOFX",true);
				
				System.err.print("creating:");
				Util.printEntity(ent);
					
				ds.put(ent);
			}

			
			
		} catch (IOException ex) {
			status.setText("Error: " + ex.getMessage());
			ex.printStackTrace();
			
			return false;
			
		} finally {
			installer.uninstall();
		}

		return true;
	}

}
